package org.codehaus.mojo.animal_sniffer.maven;

/*
 * The MIT License
 *
 * Copyright (c) 2008 Kohsuke Kawaguchi and codehaus.org.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 */

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.artifact.filter.PatternExcludesArtifactFilter;
import org.apache.maven.shared.artifact.filter.PatternIncludesArtifactFilter;
import org.codehaus.mojo.animal_sniffer.ClassFileVisitor;
import org.codehaus.mojo.animal_sniffer.ClassListBuilder;
import org.codehaus.mojo.animal_sniffer.Clazz;
import org.codehaus.mojo.animal_sniffer.SignatureChecker;
import org.codehaus.plexus.util.StringUtils;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.resolution.ArtifactRequest;
import org.eclipse.aether.resolution.ArtifactResolutionException;
import org.eclipse.aether.resolution.ArtifactResult;

/**
 * Checks the classes compiled by this module.
 *
 * @author Kohsuke Kawaguchi
 */
@Mojo(
        name = "check",
        defaultPhase = LifecyclePhase.PROCESS_TEST_CLASSES,
        requiresDependencyResolution = ResolutionScope.TEST,
        threadSafe = true)
public class CheckSignatureMojo extends AbstractMojo {

    /**
     * The directory for compiled classes.
     */
    @Parameter(defaultValue = "${project.build.outputDirectory}", required = true, readonly = true)
    protected File outputDirectory;

    /**
     * The directory for compiled test classes.
     *
     * @since 1.19
     */
    @Parameter(defaultValue = "${project.build.testOutputDirectory}", required = true, readonly = true)
    protected File testOutputDirectory;

    /**
     * Should test classes be checked.
     *
     * @since 1.19
     */
    @Parameter(property = "animal.sniffer.checkTestClasses", defaultValue = "false")
    protected boolean checkTestClasses;

    /**
     * Signature module to use.
     */
    @Parameter(required = true, property = "animal.sniffer.signature")
    protected Signature signature;

    /**
     * @param signatureId
     *            A fully-qualified path to a signature jar. This allows users
     *            to set a signature for command-line invocations, such as:
     *            <p>
     *            <code>mvn org.codehaus.mojo:animal-sniffer-maven-plugin:1.15:check -Dsignature=org.codehaus.mojo.signature:java17:1.0</code>
     */
    public void setSignature(String signatureId) {
        String[] signatureParts = signatureId.split(":");
        if (signatureParts.length == 3) {
            this.signature = new Signature();
            this.signature.setGroupId(signatureParts[0]);
            this.signature.setArtifactId(signatureParts[1]);
            this.signature.setVersion(signatureParts[2]);
        }
    }

    /**
     * Class names to ignore signatures for (wildcards accepted).
     *
     */
    @Parameter
    protected String[] ignores;

    /**
     * Annotation names to consider to ignore annotated methods, classes or fields.
     * <p>
     * By default 'org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement' and
     * 'org.jvnet.animal_sniffer.IgnoreJRERequirement' are used.
     *
     * @see SignatureChecker#ANNOTATION_FQN
     * @see SignatureChecker#PREVIOUS_ANNOTATION_FQN
     */
    @Parameter
    protected String[] annotations;

    /**
     * Should dependencies be ignored.
     *
     */
    @Parameter(defaultValue = "true")
    protected boolean ignoreDependencies;

    /**
     * A list of artifact patterns to include. Patterns can include <code>*</code> as a wildcard match for any
     * <b>whole</b> segment, valid patterns are:
     * <ul>
     * <li><code>groupId:artifactId</code></li>
     * <li><code>groupId:artifactId:type</code></li>
     * <li><code>groupId:artifactId:type:version</code></li>
     * <li><code>groupId:artifactId:type:classifier</code></li>
     * <li><code>groupId:artifactId:type:classifier:version</code></li>
     * </ul>
     *
     * @since 1.12
     */
    @Parameter
    private String[] includeDependencies = null;

    /**
     * A list of artifact patterns to exclude. Patterns can include <code>*</code> as a wildcard match for any
     * <b>whole</b> segment, valid patterns are:
     * <ul>
     * <li><code>groupId:artifactId</code></li>
     * <li><code>groupId:artifactId:type</code></li>
     * <li><code>groupId:artifactId:type:version</code></li>
     * <li><code>groupId:artifactId:type:classifier</code></li>
     * <li><code>groupId:artifactId:type:classifier:version</code></li>
     * </ul>
     *
     * @since 1.12
     */
    @Parameter
    private String[] excludeDependencies = null;

    /**
     * Should signature checking be skipped?
     *
     */
    @Parameter(defaultValue = "false", property = "animal.sniffer.skip")
    protected boolean skip;

    /**
     * Should signature check failures throw an error?
     *
     */
    @Parameter(defaultValue = "true", property = "animal.sniffer.failOnError")
    protected boolean failOnError;

    /**
     */
    @Parameter(defaultValue = "${project}", readonly = true)
    protected MavenProject project;

    @Parameter(defaultValue = "${repositorySystemSession}", readonly = true)
    private RepositorySystemSession repositorySystemSession;

    @Component
    private RepositorySystem repositorySystem;

    static Map<File, Map<String, Clazz>> classes = new ConcurrentHashMap<>();

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        if (skip) {
            getLog().info("Signature checking is skipped.");
            return;
        }

        if (signature == null || StringUtils.isBlank(signature.getGroupId()) || signature.getGroupId() == "null") {
            getLog().info("Signature version is: " + signature.getVersion());
            return;
        }

        try {
            if (StringUtils.isBlank(signature.getVersion())) {
                getLog().debug("Resolving signature " + signature.getGroupId() + ":" + signature.getArtifactId()
                        + " version from dependencies");
                String source = "dependencies";
                Dependency match = findMatchingDependency(signature, project.getDependencies());
                if (match == null && project.getDependencyManagement() != null) {
                    getLog().debug("Resolving signature " + signature.getGroupId() + ":" + signature.getArtifactId()
                            + " version from dependencyManagement");
                    source = "dependencyManagement";
                    match = findMatchingDependency(
                            signature, project.getDependencyManagement().getDependencies());
                }
                if (match != null) {
                    getLog().info("Resolved signature " + signature.getGroupId() + ":" + signature.getArtifactId()
                            + " version as " + match.getVersion() + " from " + source);
                    signature.setVersion(match.getVersion());
                }
            }

            getLog().info("Checking unresolved references to " + signature);

            File signatureFile = resolveFileForArtifact(signature.createArtifact());

            // just check code from this module
            final Set<String> ignoredPackages = buildPackageList();

            if (ignores != null) {
                for (String ignore : ignores) {
                    if (ignore == null) {
                        continue;
                    }
                    ignoredPackages.add(ignore.replace('.', '/'));
                }
            }

            final SignatureChecker signatureChecker =
                    new SignatureChecker(loadClasses(signatureFile), ignoredPackages, new MavenLogger(getLog()));
            signatureChecker.setCheckJars(false); // don't want to decend into jar files that have been copied to
            // the output directory as resources.

            signatureChecker.setSourcePath(buildSourcePathList());

            if (annotations != null) {
                signatureChecker.setAnnotationTypes(Arrays.asList(annotations));
            }

            if (checkTestClasses) {
                signatureChecker.process(new File[] {outputDirectory, testOutputDirectory});
            } else {
                signatureChecker.process(outputDirectory);
            }

            if (signatureChecker.isSignatureBroken()) {
                if (failOnError) {
                    throw new MojoFailureException(
                            "Signature errors found. Verify them and ignore them with the proper annotation if needed.");
                } else {
                    getLog().info(
                                    "Signature errors found. Verify them and ignore them with the proper annotation if needed.");
                }
            } else {
                getLog().debug("No signature errors");
            }
        } catch (IOException e) {
            throw new MojoExecutionException("Failed to check signatures", e);
        } catch (ArtifactResolutionException e) {
            throw new MojoExecutionException("Failed to obtain signature: " + signature, e);
        }
    }

    private File resolveFileForArtifact(org.eclipse.aether.artifact.Artifact artifact)
            throws ArtifactResolutionException, MojoExecutionException {

        if (StringUtils.isBlank(artifact.getVersion())) {
            throw new MojoExecutionException("For artifact {" + artifact + "}: The version cannot be empty.");
        }

        ArtifactRequest request = new ArtifactRequest(artifact, project.getRemotePluginRepositories(), null);
        ArtifactResult result = repositorySystem.resolveArtifact(repositorySystemSession, request);
        return result.getArtifact().getFile();
    }

    private static Map<String, Clazz> loadClasses(File f) throws IOException {
        Map<String, Clazz> classes = CheckSignatureMojo.classes.get(f);
        if (classes == null) {
            classes = SignatureChecker.loadClasses(new FileInputStream(f));
            CheckSignatureMojo.classes.putIfAbsent(f, classes);
        }
        return classes;
    }

    private static Dependency findMatchingDependency(Signature signature, List<Dependency> dependencies) {
        Dependency match = null;
        for (Dependency d : dependencies) {
            if (StringUtils.isBlank(d.getVersion())) {
                continue;
            }
            if (StringUtils.equals(d.getGroupId(), signature.getGroupId())
                    && StringUtils.equals(d.getArtifactId(), signature.getArtifactId())) {
                if ("signature".equals(d.getType())) {
                    // this is a perfect match
                    match = d;
                    break;
                }
                if ("pom".equals(d.getType())) {
                    if (match == null || "jar".equals(match.getType())) {
                        match = d;
                    }
                }
                if ("jar".equals(d.getType())) {
                    if (match == null) {
                        match = d;
                    }
                }
            }
        }
        return match;
    }

    /**
     * List of packages defined in the application.
     */
    private Set<String> buildPackageList() throws IOException {
        ClassListBuilder plb = new ClassListBuilder(new MavenLogger(getLog()));
        apply(plb);
        return plb.getPackages();
    }

    private void apply(ClassFileVisitor v) throws IOException {
        v.process(outputDirectory);
        if (checkTestClasses) {
            v.process(testOutputDirectory);
        }
        if (ignoreDependencies) {
            PatternIncludesArtifactFilter includesFilter = includeDependencies == null
                    ? null
                    : new PatternIncludesArtifactFilter(Arrays.asList(includeDependencies));
            PatternExcludesArtifactFilter excludesFilter = excludeDependencies == null
                    ? null
                    : new PatternExcludesArtifactFilter(Arrays.asList(excludeDependencies));

            getLog().debug("Building list of classes from dependencies");

            Set<String> classpathScopes = new HashSet<>(
                    Arrays.asList(Artifact.SCOPE_COMPILE, Artifact.SCOPE_PROVIDED, Artifact.SCOPE_SYSTEM));
            if (checkTestClasses) {
                classpathScopes.addAll(Arrays.asList(Artifact.SCOPE_TEST, Artifact.SCOPE_RUNTIME));
            }

            for (Artifact artifact : (Iterable<Artifact>) project.getArtifacts()) {

                if (!artifact.getArtifactHandler().isAddedToClasspath()) {
                    getLog().debug("Skipping artifact " + BuildSignaturesMojo.artifactId(artifact)
                            + " as it is not added to the classpath.");
                    continue;
                }

                if (!classpathScopes.contains(artifact.getScope())) {
                    getLog().debug("Skipping artifact " + BuildSignaturesMojo.artifactId(artifact)
                            + " as it is not on the " + (checkTestClasses ? "test" : "compile") + " classpath.");
                    continue;
                }

                if (includesFilter != null && !includesFilter.include(artifact)) {
                    getLog().debug("Skipping classes in artifact " + BuildSignaturesMojo.artifactId(artifact)
                            + " as it does not match include rules.");
                    continue;
                }

                if (excludesFilter != null && !excludesFilter.include(artifact)) {
                    getLog().debug("Skipping classes in artifact " + BuildSignaturesMojo.artifactId(artifact)
                            + " as it does matches exclude rules.");
                    continue;
                }

                getLog().debug("Adding classes in artifact " + BuildSignaturesMojo.artifactId(artifact)
                        + " to the ignores");
                v.process(artifact.getFile());
            }
        }
    }

    @SuppressWarnings("unchecked")
    private List<File> buildSourcePathList() {
        List<String> compileSourceRoots = new ArrayList<>(project.getCompileSourceRoots());
        if (checkTestClasses) {
            compileSourceRoots.addAll(project.getTestCompileSourceRoots());
        }
        List<File> sourcePathList = new ArrayList<>(compileSourceRoots.size());
        for (String compileSourceRoot : compileSourceRoots) {
            sourcePathList.add(new File(compileSourceRoot));
        }
        return sourcePathList;
    }
}
